﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using AvalonDock;

namespace BOL.Apps.GeneNetworkBrowser.Controls
{
    public partial class AvalonDockHost
    {
        #region Private variables

        private readonly Dictionary<object, ManagedContent> _contentMap = new Dictionary<object, ManagedContent>(); // a dictionary that maps from view-model object to Avalondock ManagedContent object.
        private bool _disableClosingEvent; // set to 'true' to disable the request for user confirmation while closing a document.

        #endregion

        #region Dependency properties

        public static readonly DependencyProperty PanesProperty =
            DependencyProperty.Register("Panes", typeof(IList), typeof(AvalonDockHost),
                new FrameworkPropertyMetadata(DocumentsOrPanesPropertyChanged));

        public static readonly DependencyProperty DocumentsProperty =
            DependencyProperty.Register("Documents", typeof(IList), typeof(AvalonDockHost),
                new FrameworkPropertyMetadata(DocumentsOrPanesPropertyChanged));

        public static readonly DependencyProperty ActiveDocumentProperty =
            DependencyProperty.Register("ActiveDocument", typeof(object), typeof(AvalonDockHost),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ActiveDocumentOrPanePropertyChanged));

        public static readonly DependencyProperty ActivePaneProperty =
            DependencyProperty.Register("ActivePane", typeof(object), typeof(AvalonDockHost),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ActiveDocumentOrPanePropertyChanged));

        public static readonly DependencyProperty IsPaneVisibleProperty =
            DependencyProperty.RegisterAttached("IsPaneVisible", typeof(bool), typeof(AvalonDockHost),
                new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, IsPaneVisiblePropertyChanged));

        #endregion

        #region Public properties

        public IList Panes { get { return (IList)GetValue(PanesProperty); } set { SetValue(PanesProperty, value); } }

        public IList Documents { get { return (IList)GetValue(DocumentsProperty); } set { SetValue(DocumentsProperty, value); } }

        public object ActivePane { get { return GetValue(ActivePaneProperty); } set { SetValue(ActivePaneProperty, value); } }

        public object ActiveDocument { get { return GetValue(ActiveDocumentProperty); } set { SetValue(ActiveDocumentProperty, value); } }

        #endregion

        #region EventHandlers

        // Event raised when Avalondock has loaded.
        public event EventHandler<EventArgs> AvalonDockLoaded;

        // Event raised when a document is being closed by clicking on the AvalonDock X button .
        public event EventHandler<DocumentClosingEventArgs> DocumentClosing;

        #endregion

        #region Constructor

        public AvalonDockHost()
        {
            InitializeComponent();

            // hook the AvalonDock event that is raised when the focused content is changed.
            _dockingManager.ActiveContentChanged += DockingManagerActiveContentChanged;
            UpdateActiveContent();
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Event raised when the Documents or Panes properties have changed.
        /// </summary>
        private static void DocumentsOrPanesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var c = (AvalonDockHost)d;

            // Deal with the previous value of the property.
            if (e.OldValue != null)
            {
                // Remove the old panels from AvalonDock.
                var oldPanels = (IList)e.OldValue;
                c.RemovePanels(oldPanels);

                var observableCollection = oldPanels as INotifyCollectionChanged;
                if (observableCollection != null)
                    observableCollection.CollectionChanged -= c.DocumentsOrPanesCollectionChanged; // Unhook the CollectionChanged event, we no longer need to receive notifications of modifications to the collection.
            }

            // Deal with the new value of the property.
            if (e.NewValue != null)
            {
                // Add the new panels to AvalonDock.
                var newPanels = (IList)e.NewValue;
                c.AddPanels(newPanels);

                var observableCollection = newPanels as INotifyCollectionChanged;
                if (observableCollection != null)
                    observableCollection.CollectionChanged += c.DocumentsOrPanesCollectionChanged; // Hook the CollectionChanged event to receive notifications of future modifications to the collection.
            }
        }

        /// <summary>
        /// Event raised when the 'Documents' or 'Panes' collection have had items added/removed.
        /// </summary>
        private void DocumentsOrPanesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Reset)
                ResetDocumentsOrPanes(sender); // The collection has been cleared, need to remove all documents or panes from AvalonDock depending on which collection was actually cleared.
            else
            {
                if (e.OldItems != null)
                    RemovePanels(e.OldItems); // Remove the old panels from AvalonDock.
                if (e.NewItems != null)
                    AddPanels(e.NewItems); // Add the new panels to AvalonDock.
            }
        }

        /// <summary>
        /// This method resets documents or panes in response
        /// to the Reset action of the CollectionChanged events.
        /// </summary>
        private void ResetDocumentsOrPanes(object sender)
        {
            if (sender == Documents)
            {
                // We are clearing the collection of documents.
                foreach (var item in _contentMap.ToArray())
                    if (item.Value is DocumentContent)
                        RemovePanel(item.Key); // Only remove DocumentContent components.
            }
            else if (sender == Panes)
            {
                // We are clearing the collection of panes.
                foreach (var item in _contentMap.ToArray())
                    if (item.Value is DockableContent)
                        RemovePanel(item.Key); // Only remove DockableContent components.
            }
            else
                throw new ApplicationException("Unexpected CollectionChanged event sender: " + sender.GetType().Name);
        }

        /// <summary>
        /// Add panels to Avalondock.
        /// </summary>
        private void AddPanels(IList panels)
        {
            foreach (var panel in panels)
                AddPanel(panel);
        }

        /// <summary>
        /// Add a panel to Avalondock.
        /// </summary>
        private void AddPanel(object panel)
        {
            // Instantiate a UI element based on the panel's view-model type.
            // The visual-tree is searched to find a DataTemplate keyed to the requested type.
            var panelViewModelType = panel.GetType();
            var uiElement = DataTemplateUtils.InstanceTemplate(panelViewModelType, this, panel);
            
            if (uiElement == null)
                throw new ApplicationException("Failed to find data-template for type: " + panel.GetType().Name);

            // Cast the instantiated UI element to an AvalonDock ManagedContent.
            // ManagedContent can refer to either an AvalonDock DocumentContent
            // or an AvalonDock DockableContent.
            var managedContent = uiElement as ManagedContent;
            if (managedContent == null)
                throw new ApplicationException("Found data-template for type: " + panel.GetType().Name + ", but the UI element generated is not a ManagedContent (base-class of DocumentContent/DockableContent), rather it is a " + uiElement.GetType().Name);

            // Associate the panel's view-model with the Avalondock ManagedContent so it can be retrieved later.
            _contentMap[panel] = managedContent;

            // Hook the event to track when the document has been closed.
            managedContent.Closed += new EventHandler(ManagedContent_Closed);

            var documentContent = managedContent as DocumentContent;
            if (documentContent != null)
            {
                // For documents only, hook Closing so that the application can be informed
                // when a document is in the process of being closed by the use clicking the
                // AvalonDock close button.
                documentContent.Closing += new EventHandler<CancelEventArgs>(DocumentContent_Closing);
            }
            else
            {
                var dockableContent = managedContent as DockableContent;
                if (dockableContent != null)
                    dockableContent.StateChanged += new RoutedEventHandler(DockableContentState_Changed); // For panes only, hook StateChanged so we know when a DockableContent is shown/hidden.
                else
                    throw new ApplicationException(String.Format("Panel {0} is expected to be either DocumentContent or DockableContent.", managedContent.GetType().Name));
            }

            managedContent.Show(_dockingManager);
            managedContent.Activate();
        }

        /// <summary>
        /// Remove panels from Avalondock.
        /// </summary>
        private void RemovePanels(IList panels)
        {
            foreach (var panel in panels)
                RemovePanel(panel);
        }

        /// <summary>
        /// Remove a panel from Avalondock.
        /// </summary>
        private void RemovePanel(object panel)
        {
            // Look up the document in the content map.
            ManagedContent managedContent = null;
            if (_contentMap.TryGetValue(panel, out managedContent))
            {
                _disableClosingEvent = true;

                try
                {
                    // The content was still in the map, and therefore still open, so close it.
                    managedContent.Close();
                }
                finally
                {
                    _disableClosingEvent = false;
                }
            }
        }

        /// <summary>
        /// Event raised when an AvalonDock DocumentContent is being closed.
        /// </summary>
        private void DocumentContent_Closing(object sender, CancelEventArgs e)
        {
            var documentContent = sender as DocumentContent;
            var document = documentContent.DataContext;

            if (!_disableClosingEvent)
            {
                if (DocumentClosing != null)
                {
                    // Notify the application that the document is being closed.
                    var eventArgs = new DocumentClosingEventArgs(document);
                    DocumentClosing(this, eventArgs);

                    if (eventArgs.Cancel)
                    {
                        // Closing of the document is to be cancelled.
                        e.Cancel = true;
                        return;
                    }
                }
            }

            documentContent.Closing -= new EventHandler<CancelEventArgs>(DocumentContent_Closing);
        }

        /// <summary>
        /// Event raised when an Avalondock ManagedContent has been closed.
        /// </summary>
        private void ManagedContent_Closed(object sender, EventArgs e)
        {
            var managedContent = (ManagedContent)sender;
            var content = managedContent.DataContext;

            // Remove the content from the content map right now.
            // There is no need to keep it around any longer.
            _contentMap.Remove(content);

            managedContent.Closed -= new EventHandler(ManagedContent_Closed);

            var documentContent = managedContent as DocumentContent;
            if (documentContent != null)
            {
                Documents.Remove(content);

                if (ActiveDocument == content)
                    ActiveDocument = null; // Active document has closed, clear it.
            }
            else
            {
                var dockableContent = managedContent as DockableContent;
                if (dockableContent != null)
                {
                    // For panes only, unhook StateChanged event.
                    dockableContent.StateChanged -= new RoutedEventHandler(DockableContentState_Changed);

                    Panes.Remove(content);

                    if (ActivePane == content)
                        ActivePane = null; // Active pane has closed, clear it.
                }
            }
        }

        /// <summary>
        /// Event raised when the 'dockable state' of a DockableContent has changed.
        /// </summary>
        private void DockableContentState_Changed(object sender, RoutedEventArgs e)
        {
            var dockableContent = (DockableContent)sender;
            SetPaneDockableContentState(dockableContent, dockableContent.State != DockableContentState.Hidden);
        }

        /// <summary>
        /// Update the active pane and document from the currently active AvalonDock component.
        /// </summary>
        private void UpdateActiveContent()
        {
            var activePane = _dockingManager.ActiveContent as DockableContent;
            if (activePane != null)
                ActivePane = activePane.DataContext; // Set the active document so that we can bind to it.
            else
            {
                var activeDocument = _dockingManager.ActiveContent as DocumentContent;
                if (activeDocument != null)
                {
                    ActiveDocument = activeDocument.DataContext; // Set the active document so that we can bind to it.
                }
            }
        }

        /// <summary>
        /// Event raised when the IsPaneVisible property changes.
        /// </summary>
        private static void IsPaneVisiblePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var avalonDockContent = o as ManagedContent;
            if (avalonDockContent != null)
            {
                bool isVisible = (bool)e.NewValue;
                if (isVisible)
                    avalonDockContent.Show();
                else
                    avalonDockContent.Hide();
            }
        }

        /// <summary>
        /// Event raised when the active content has changed.
        /// </summary>
        private void DockingManagerActiveContentChanged(object sender, EventArgs e)
        {
            UpdateActiveContent();
        }

        /// <summary>
        /// Event raised when the ActiveDocument or ActivePane property has changed.
        /// </summary>
        private static void ActiveDocumentOrPanePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var c = (AvalonDockHost)d;
            ManagedContent managedContent = null;

            if (e.NewValue != null && c._contentMap.TryGetValue(e.NewValue, out managedContent))
                managedContent.Activate();
        }

        /// <summary>
        /// Event raised when Avalondock has loaded.
        /// </summary>
        private void AvalonDock_Loaded(object sender, RoutedEventArgs e)
        {
            if (AvalonDockLoaded != null)
                AvalonDockLoaded(this, EventArgs.Empty);
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Sets the PaneDockableContentState for an element.
        /// </summary>
        public static void SetPaneDockableContentState(UIElement element, bool value)
        {
            element.SetValue(IsPaneVisibleProperty, value);
        }

        /// <summary>
        /// Gets the PaneDockableContentState property for an element.
        /// </summary>
        public static bool GetPaneDockableContentState(UIElement element)
        {
            return (bool)element.GetValue(IsPaneVisibleProperty);
        }

        #endregion
    }
}
